package com.tianji.learning.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.api.client.course.CourseClient;
import com.tianji.api.dto.course.CourseFullInfoDTO;
import com.tianji.api.dto.leanring.LearningLessonDTO;
import com.tianji.api.dto.leanring.LearningRecordDTO;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.exceptions.DbException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.learning.domain.dto.LearningRecordFormDTO;
import com.tianji.learning.domain.po.LearningLesson;
import com.tianji.learning.domain.po.LearningRecord;
import com.tianji.learning.enums.LessonStatus;
import com.tianji.learning.enums.SectionType;
import com.tianji.learning.mapper.LearningRecordMapper;
import com.tianji.learning.service.ILearningLessonService;
import com.tianji.learning.service.ILearningRecordService;
import com.tianji.learning.util.LearningRecordDelayTaskHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * <p>
 * 学习记录表 服务实现类
 * </p>
 *
 * @author zr
 * @since 2024-07-11
 */
@Service
@RequiredArgsConstructor
public class LearningRecordServiceImpl extends ServiceImpl<LearningRecordMapper, LearningRecord> implements ILearningRecordService {
    private final ILearningLessonService lessonService;
    private final CourseClient courseClient;
    private final RabbitMqHelper mqHelper;
    private final LearningRecordDelayTaskHandler taskHandler;

    /**
     * 根据课程ID查询学习记录。
     * <p>
     * 本方法通过课程ID检索与该课程相关的学习记录详情。学习记录数据传输对象（DTO）
     * 包含了学习记录的详细信息
     *
     * @param courseId 课程的唯一标识ID。此ID用于精确匹配课程学习记录。
     * @return LearningLessonDTO 学习记录的数据传输对象，包含了关于学习进度和表现的详细信息。
     */
    @Override
    public LearningLessonDTO queryLearningRecordByCourse(Long courseId) {
        // 1.获取登录用户
        Long userId = UserContext.getUser();
        // 2.查询课表
        LearningLesson lesson = lessonService.queryByUserAndCourseId(userId, courseId);
        if(lesson == null){
            throw new BizIllegalException("该用户未拥有该课程");
        }
        // 3.查询学习记录
        // select * from xx where lesson_id = #{lessonId}
        List<LearningRecord> records = lambdaQuery().eq(LearningRecord::getLessonId, lesson.getId()).list();
        // 4.封装结果
        LearningLessonDTO dto = new LearningLessonDTO();
        dto.setId(lesson.getId());
        dto.setLatestSectionId(lesson.getLatestSectionId());
        dto.setRecords(BeanUtils.copyList(records, LearningRecordDTO.class));
        return dto;
    }

    /**
     * 添加学习记录。
     * <p>
     * 通过此方法，可以将学习记录的相关信息存储到系统中。
     * 学习记录对于跟踪用户学习进度。
     *
     * @param formDTO 学习记录的表单数据传输对象。包含了关于学习记录的详细信息，
     *                如学习内容、学习时间等。这个参数不允许为空，否则无法正确添加学习记录。
     */
    @Override
    @Transactional
    public void addLearningRecord(LearningRecordFormDTO formDTO) {
        // 1.获取登录用户
        Long userId = UserContext.getUser();
        // 2.处理学习记录
        boolean isFinished = false;
        if(formDTO.getSectionType().equals(SectionType.VIDEO)){
            // 2.1.处理视频
            isFinished = handleVideoRecord(userId,formDTO);
        }else{
            // 2.2.处理考试
            isFinished = handleExamRecord(userId,formDTO);
        }
        if(!isFinished){
            // 没有新学完的小节，无需更新课表中的学习进度
            return;
        }
        // 3.处理课表数据
        handleLearningLessonsChanges(formDTO);
    }
    private void handleLearningLessonsChanges(LearningRecordFormDTO recordDTO) {
        // 1.查询课表
        LearningLesson lesson = lessonService.getById(recordDTO.getLessonId());
        if (lesson == null) {
            throw new BizIllegalException("课程不存在，无法更新数据！");
        }
        // 2.判断是否有新的完成小节
        boolean allLearned = false;
        // 3.如果有新完成的小节，则需要查询课程数据
        CourseFullInfoDTO cInfo = courseClient.getCourseInfoById(lesson.getCourseId(), false, false);
        if (cInfo == null) {
            throw new BizIllegalException("课程不存在，无法更新数据！");
        }
        // 4.比较课程是否全部学完：已学习小节 >= 课程总小节
        allLearned = lesson.getLearnedSections() + 1 >= cInfo.getSectionNum();
        // 5.更新课表
        lessonService.lambdaUpdate()
                // 5.1由于购买课程后就会初始化lesson，且初始化已学小节为0，所以如果是第一次观看视频，则设置lesson为正在学习
                .set(lesson.getLearnedSections() == 0, LearningLesson::getStatus, LessonStatus.LEARNING)
                // 5.2如果小节都学完了就更新lesson为完成状态
                .set(allLearned, LearningLesson::getStatus, LessonStatus.FINISHED)
                // 5.3更新最后学习时间
                .set(LearningLesson::getLatestLearnTime, recordDTO.getCommitTime())
                // 5.4更新最后学习章节
                .set(LearningLesson::getLatestSectionId, recordDTO.getSectionId())
                .setSql("learned_sections = learned_sections + 1")
                .eq(LearningLesson::getId, lesson.getId())
                .update();
    }
    /**
     * 【已废弃！】更新课表方法
     * @param formDTO 当前课表
     * @param isFinished 是否学完
     */
    private void handleLearningLessonsChange_(LearningRecordFormDTO formDTO, boolean isFinished) {
        // 1.查询课表
        LearningLesson lesson = lessonService.getById(formDTO.getLessonId());
        if (lesson == null) {
            throw new BizIllegalException("课程不存在，无法更新数据！");
        }
        // 2.判断是否有新的完成小节
        boolean allLearned = false;
        if (isFinished) {
            // 3.如果有新完成的小节，则需要查询课程数据
            CourseFullInfoDTO cInfo = courseClient.getCourseInfoById(lesson.getCourseId(), false, false);
            if (cInfo == null) {
                throw new BizIllegalException("课程不存在，无法更新数据！");
            }
            // 4.比较课程是否全部学完：已学习小节 >= 课程总小节
            allLearned = lesson.getLearnedSections() + 1 >= cInfo.getSectionNum();
        }
        // 5.更新课表
        boolean update = lessonService.lambdaUpdate()
                // 5.1由于购买课程后就会初始化lesson，且初始化已学小节为0，所以如果是第一次观看视频，则设置lesson为正在学习
                .set(lesson.getLearnedSections() == 0, LearningLesson::getStatus, LessonStatus.LEARNING)
                // 5.2如果小节都学完了就更新lesson为完成状态
                .set(allLearned, LearningLesson::getStatus, LessonStatus.FINISHED)
                // 5.3更新最后学习时间
                .set(LearningLesson::getLatestLearnTime, formDTO.getCommitTime())
                // 5.4更新最后学习章节
                .set(LearningLesson::getLatestSectionId, formDTO.getSectionId())
                //如果完成了小节则课表完成小节数量+1
//                .set(isFinished, LearningLesson::getLearnedSections, lesson.getLearnedSections()+1)
                .setSql(isFinished, "learned_sections = learned_sections + 1")
                .eq(LearningLesson::getId, lesson.getId())
                .update();
        if(!update){
            throw new DbException("课表更新失败");
        }
    }
    /**
     * 【已废弃！】更新学习记录，数据库操作版本
     * 缺点：如果多用户持续观看视频就会导致查、改数据库请求过大
     * 心跳是每15秒发送一次, 并发量可能会过大, 会操作两次数据库:
     * + 查询学习记录表
     * + 修改学习记录表
     * 所以需要优化为异步延时阻塞队列版本(优先查Redis记录->修改Redis记录->最后用户离开再把Redis记录持久化到数据库)
     * @param userId 用户id
     * @param formDTO 当前课表
     * @return 是否学完
     */
    private boolean handleVideoRecord_(Long userId, LearningRecordFormDTO formDTO) {
        // 1.查询旧的学习记录
        LearningRecord learningRecord = lambdaQuery()
                .eq(LearningRecord::getLessonId, formDTO.getLessonId())
                .eq(LearningRecord::getSectionId, formDTO.getSectionId())
                .one();
        // 1.1.判断是否存在
        if(learningRecord==null){
            LearningRecord record= BeanUtils.copyBean(formDTO, LearningRecord.class);
            record.setUserId(userId);
            boolean result = save(record);
            if(!result){
                throw new DbException("新增学习记录失败");
            }
            return false; // 代表本小节没学完
        }
        //2.有学习记录，修改学习记录
        //2.1如果该小节视频长度完成50%及以上且小节旧的状态为未完成，才让当前课程表的小节完成数量+1
        boolean finished = !learningRecord.getFinished() && formDTO.getMoment() * 2 >= formDTO.getDuration();
        //2.2更新小节学习，如果符合完成条件，则更新小节记录的完成时间和状态
        boolean update = lambdaUpdate()
                .set(LearningRecord::getMoment, formDTO.getMoment())
                .set(finished, LearningRecord::getFinished, true)
                .set(finished, LearningRecord::getFinishTime, formDTO.getCommitTime())
                .eq(LearningRecord::getId, learningRecord.getId())
                .update();
        if(!update){
            throw new DbException("更新学习记录失败");
        }
        return finished;
    }

    /**
     * 异步延时阻塞队列版本(大致思路：优先查Redis记录->修改Redis记录->最后用户离开再把Redis记录持久化到数据库)
     * 减少了对数据库的操作，避免高并发
     * @param userId 用户Id
     * @param recordDTO 当前课表
     * @return 是否学完
     */
    private boolean handleVideoRecord(Long userId, LearningRecordFormDTO recordDTO) {
        // 1.查询旧的学习记录
        LearningRecord old = queryOldRecord(recordDTO.getLessonId(), recordDTO.getSectionId());
        // 2.判断是否存在
        if (old == null) {
            // 3.不存在，则新增
            // 3.1.转换PO
            LearningRecord record = BeanUtils.copyBean(recordDTO, LearningRecord.class);
            // 3.2.填充数据
            record.setUserId(userId);
            // 3.3.写入数据库
            boolean success = save(record);
            if (!success) {
                throw new DbException("新增学习记录失败！");
            }
            return false;
        }
        // 4.存在，则更新
        // 4.1.判断是否是第一次完成
        boolean finished = !old.getFinished() && recordDTO.getMoment() * 2 >= recordDTO.getDuration();
        if(!finished){
            LearningRecord record = new LearningRecord();
            record.setLessonId(recordDTO.getLessonId());
            record.setSectionId(recordDTO.getSectionId());
            record.setMoment(recordDTO.getMoment());
            record.setId(old.getId());
            record.setFinished(old.getFinished());
            taskHandler.addLearningRecordTask(record);
            //如果不返回会执行另一个分支流程->重复更新小节学习记录
            return false;
        }
        // 首次完成视频播放，可以增加积分，发送MQ消息实现
        // 发送MQ消息实现观看学习视频获取积分
        mqHelper.send(MqConstants.Exchange.LEARNING_EXCHANGE,
                MqConstants.Key.LEARN_SECTION,
                userId);   // 学习一个视频 + 10积分
        // 4.2.更新数据
        boolean success = lambdaUpdate()
                .set(LearningRecord::getMoment, recordDTO.getMoment())
                .set(LearningRecord::getFinished, true)
                .set(LearningRecord::getFinishTime, recordDTO.getCommitTime())
                .eq(LearningRecord::getId, old.getId())
                .update();
        if(!success){
            throw new DbException("更新学习记录失败！");
        }
        // 4.3.清理缓存
        taskHandler.cleanRecordCache(recordDTO.getLessonId(), recordDTO.getSectionId());
        return true;
    }

    private LearningRecord queryOldRecord(Long lessonId, Long sectionId) {
        // 1.查询缓存
        LearningRecord record = taskHandler.readRecordCache(lessonId, sectionId);
        // 2.如果命中，直接返回
        if (record != null) {
            return record;
        }
        // 3.未命中，查询数据库
        LearningRecord dbRecord = lambdaQuery()
                .eq(LearningRecord::getLessonId, lessonId)
                .eq(LearningRecord::getSectionId, sectionId)
                .one();
        if(dbRecord==null){
            return null;
        }
        // 4.写入缓存
        taskHandler.writeRecordCache(dbRecord);
        return dbRecord;
    }

    private boolean handleExamRecord(Long userId, LearningRecordFormDTO formDTO) {
        LearningRecord record = BeanUtils.copyBean(formDTO, LearningRecord.class);
        record.setUserId(userId);
        record.setFinished(true);
        record.setFinishTime(formDTO.getCommitTime());
        boolean result = save(record);
        if(!result){
            throw new DbException("新增考试记录失败");
        }
        return true;
    }
}